package org.zachary.imitation.raft.entity;

import org.zachary.imitation.raft.Configuration;
import org.zachary.imitation.raft.PeerId;

import java.util.ArrayList;
import java.util.List;

/**
 * 计算投票结果的类，整个类的原理实际上是做减法。得到集群中所有参与选举的节点，然后计算出最少需要的票数，每得到一票就从需要得到的票数中减 1，直到减为 0，就说明获得了足够的票数了
 */
public class Ballot {

	// 存放当前集群配置中的所有节点的信息
	private final List<UnFoundPeerId> peers = new ArrayList<>();
	// 当前集群配置中需要收到的最小票数，才能成功当选领导者或者进入正式投票节点
	private int quorum;
	// 存放旧集群配置中的所有节点的信息
	private final List<UnFoundPeerId> oldPeers = new ArrayList<>();
	// 旧集群配置中需要收到的最小票数，才能成功当选领导者或者进入正式投票节点
	private int oldQuorum;

	/** 该内部类对象是用来表示当前节点的 PeerId 在集群中位置的，方便快速查找 */
	public static final class PosHint {
		// 表示当前节点在集群集合 peers 中的位置，初始化为 -1
		int pos0 = -1;
		// 如果一个节点是旧配置中的，那么就用下面这个成员变量代表就旧集群集合 oldPeers 的位置
		int pos1 = -1;
	}

	/** 该内部类会封装节点的 PeerId，并且给这个节点添加一个未找到的标记，然后把这个节点存放到集合中。如果节点是属于当前配置中的就存放到 peers 集合中，否则存放到 oldPeers 集合中
	 *  等当前节点收到集群集合 peers 中存放的节点的投票时，会去集合中查找，看看有没有对应的节点。如果有就把该节点标记为已找到，并且把投票数计入结果，说明成功收到投票了 */
	public static class UnFoundPeerId {
		// 节点信息
		PeerId peerId;
		// 节点索引，参考 Ballot#init 中的逻辑
		int index;
		// 节点是否被找到，UnFoundPeerId 对象刚创建的时候，该变量为 false，表示该节点对象还没被找到
		boolean found;

		public UnFoundPeerId(PeerId peerId, int index, boolean found) {
			super();
			this.peerId = peerId;
			this.index = index;
			this.found = found;
		}
	}

	/** 该类最核心的方法，初始化该类的对象时，会把集群中的节点信息封装到该类中，并且根据集群中节点数量计算出当前节点至少需要收到多少票数，才能成功当选领导者或者进入正式投票阶段 */
	public boolean init(final Configuration conf, final Configuration oldConf) {
		// 先清空两个集合
		this.peers.clear();
		this.oldPeers.clear();
		// 需要的最小票数也清零
		this.quorum = this.oldQuorum = 0;

		// 定义节点索引，该索引会在创建 UnFoundPeerId 对象时一直递增，当作每一个节点的索引
		int index = 0;
		// 将当前集群配置中的节点信息存放到集合中
		if (conf != null) {
			for (final PeerId peer : conf.getPeers()) {
				this.peers.add(new UnFoundPeerId(peer, index++, false));
			}
		}

		// 计算出最少需要的票数才能成功当选或者进入正式投票阶段，这里的成员变量是针对当前配置的
		this.quorum = this.peers.size() / 2 + 1;

		// 判断旧集群配置是否为空
		if (oldConf == null) {
			return true;
		}
		index = 0;
		// 如果旧集群配置不为空，则执行和上面相同的逻辑
		for (final PeerId peer : oldConf.getPeers()) {
			this.oldPeers.add(new UnFoundPeerId(peer, index++, false));
		}
		this.oldQuorum = this.oldPeers.size() / 2 + 1;
		return true;
	}

	/** 根据给定的 PeerId，判断集合中是否存在对应节点 */
	private UnFoundPeerId findPeer(final PeerId peerId, final List<UnFoundPeerId> peers, final int posHint) {
		// 判断代表节点在集合中的位置是否是正确，当一个节点被包装成 UnFoundPeerId 时，它在集合中的位置是 -1 是不合理的，这种情况下应该遍历集合，根据对应的 peerId 找出对应节点，然后根据节点的 index 来更新自己在集合中的位置（即更新 PosHint 对象中的索引）
		if (posHint < 0 || posHint >= peers.size() || !peers.get(posHint).peerId.equals(peerId)) {
			for (final UnFoundPeerId ufp : peers) {
				if (ufp.peerId.equals(peerId)) {
					return ufp;
				}
			}
			return null;
		}
		// 如果走到这里说明节点在集合中的位置已经更新了，直接根据位置获得节点即可
		return peers.get(posHint);
	}

	/** 给当前节点投票的方法，方法中传进来的这个 peerId 就是谁给当前节点投票的 */
	public void grant(final PeerId peerId) {
		// 在下面这个方法中，真正执行投票的逻辑。这里创建了一个 PosHint 对象，该对象的作用是用来表示投票者节点的 peerId 在集群集合中位置的，方便快速查找
		grant(peerId, new PosHint());
	}

	/** 给当前节点投票的方法，方法中传进来的这个 peerId 就是谁给当前节点投票的，PosHint 表示投票者节点在集群集合中的位置 */
	public PosHint grant(final PeerId peerId, final PosHint hint) {
		// 在 peers 集合中查找对应的节点
		UnFoundPeerId peer = findPeer(peerId, this.peers, hint.pos0);
		if (peer != null) {
			// 能够找到，意味着被投票的 peerId 节点是有效的，这个 peerId 在集群中
			if (!peer.found) {
				// 将这个节点标记为已找到，或者说是被发现
				peer.found = true;
				// 收到票需要的票数减1，减到0则意味着票数够了
				this.quorum--;
			}
			// 更新节点在集合中的位置
			hint.pos0 = peer.index;
		} else {
			//如果没找到，就把位置设置为-1
			hint.pos0 = -1;
		}

		// 判断旧配置的集群的节点集合是否为空
		if (this.oldPeers.isEmpty()) {
			hint.pos1 = -1;
			return hint;
		}
		//如果不为空，就执行和上面相同的逻辑
		peer = findPeer(peerId, this.oldPeers, hint.pos1);
		if (peer != null) {
			if (!peer.found) {
				peer.found = true;
				this.oldQuorum--;
			}
			hint.pos1 = peer.index;
		} else {
			hint.pos1 = -1;
		}
		return hint;
	}

	/** 判断是否收到足够的票数了，因为每收到一票 quorum 就会减 1，直到为 0 则意味着收到足够票数了 */
	public boolean isGranted() {
		return this.quorum <= 0 && this.oldQuorum <= 0;
	}
}